Micron Document
C.S.Burner 🪙 ━►🔥GIT Node


Displaying Raw • Download

meshchatx/src/frontend/components/interfaces/InterfacesPage.vue 9ec2a88817819d433c5d68dbec0ca070ca989b91 (9ec2a888) Text, 80.11 KB

T8b949e<!-- SPDX-License-Identifier: 0BSD AND MIT -->

Tff7b72<templateTff7b72>
Tff7b72<div
Te6edf3class=Ta5d6ff"Ta5d6ffflex flex-col flex-1 overflow-hidden min-w-0 bg-gradient-to-br from-slate-50 via-slate-100 to-white dark:from-zinc-950 dark:via-zinc-900 dark:to-zinc-900Ta5d6ff"
Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex-1 overflow-y-auto overflow-x-hidden w-full px-3 sm:px-5 md:px-5 lg:px-8 py-4 sm:py-6Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffspace-y-0 w-full min-w-0 max-w-6xl xl:max-w-7xl 2xl:max-w-[90rem] mx-auto flex-1Ta5d6ff"Tff7b72>
Tff7b72<div
Te6edf3v-if=Ta5d6ff"Ta5d6ffshowRestartReminderTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffbg-amber-600 text-white border border-amber-500/30 p-4 sm:rounded-xl flex flex-wrap gap-3 items-center mb-4 sm:mb-6Ta5d6ff"
Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex items-center gap-3Ta5d6ff"Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffalertTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffw-6 h-6Ta5d6ff" Tff7b72/>
Tff7b72<divTff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fftext-lg font-semiboldTa5d6ff"Tff7b72>{{ Te6edf3$t("interfaces.restart_required") }}Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fftext-smTa5d6ff"Tff7b72>{{ Te6edf3$t("interfaces.restart_description") }}Tff7b72</div>
Tff7b72</div>
Tff7b72</div>
Tff7b72<button
Te6edf3v-if=Ta5d6ff"Ta5d6ffisElectronTa5d6ff"
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffml-auto inline-flex items-center gap-2 rounded-full bg-white px-4 py-1.5 text-sm font-bold text-amber-600 hover:bg-white/90 transition shadow-smTa5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffrelaunchTa5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffrestartTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffw-4 h-4Ta5d6ff" Tff7b72/>
{{ Te6edf3$t("interfaces.restart_now") }}
Tff7b72</button>
Tff7b72</div>

Tff7b72<div
Te6edf3class=Ta5d6ff"Ta5d6ffinterfaces-section interfaces-section--hero flex flex-col lg:flex-row lg:items-center justify-between gap-6Ta5d6ff"
Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffspace-y-3 flex-1 min-w-0Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fftext-xs uppercase tracking-wide text-gray-500 dark:text-gray-400Ta5d6ff"Tff7b72>
{{ Te6edf3$t("interfaces.manage") }}
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fftext-3xl font-black text-gray-900 dark:text-white tracking-tightTa5d6ff"Tff7b72>
{{ Te6edf3$t("interfaces.title") }}
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fftext-sm text-gray-600 dark:text-gray-300 leading-relaxed max-w-xlTa5d6ff"Tff7b72>
{{ Te6edf3$t("interfaces.description") }}
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex flex-wrap gap-2 pt-2Ta5d6ff"Tff7b72>
Tff7b72<RouterLink
Te6edf3:to=Ta5d6ff"Ta5d6ff{ name: Ta5d6ff'Ta5d6ffinterfaces.addTa5d6ff'Ta5d6ff }Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffprimary-chip px-4 py-2 text-sm min-h-[44px] sm:min-h-0 items-center justify-center hidden sm:inline-flexTa5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffplusTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffw-4 h-4Ta5d6ff" Tff7b72/>
{{ Te6edf3$t("interfaces.add_interface") }}
Tff7b72</RouterLink>
Tff7b72<button Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsecondary-chip text-smTa5d6ff" @Te6edf3click=Ta5d6ff"Ta5d6ffshowImportInterfacesModalTa5d6ff"Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffimportTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffw-4 h-4Ta5d6ff" Tff7b72/>
{{ Te6edf3$t("interfaces.import") }}
Tff7b72</button>
Tff7b72<button Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsecondary-chip text-smTa5d6ff" @Te6edf3click=Ta5d6ff"Ta5d6ffexportInterfacesTa5d6ff"Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffexportTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffw-4 h-4Ta5d6ff" Tff7b72/>
{{ Te6edf3$t("interfaces.export_all") }}
Tff7b72</button>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffsecondary-chip text-sm transition-shadowTa5d6ff"
Te6edf3:class=Ta5d6ff"
Ta5d6ff showRestartReminder
? Ta5d6ff'Ta5d6ffring-2 ring-amber-400 shadow-lg shadow-amber-500/40 animate-pulse motion-reduce:animate-noneTa5d6ff'
Ta5d6ff : Ta5d6ff'Ta5d6ff'
Ta5d6ff Ta5d6ff"
Te6edf3:disabled=Ta5d6ff"Ta5d6ffreloadingRnsTa5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffreloadRnsTa5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffrestartTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffw-4 h-4Ta5d6ff" Tff7b72/>
Tff7b72<spanTff7b72>{{ reloadingRns ? Te6edf3$t("app.reloading_rns") : "Restart RNS" }}Tff7b72</span>
Tff7b72</button>
Tff7b72</div>
Tff7b72</div>

Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffw-full md:w-96 shrink-0 space-y-4Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffrelative groupTa5d6ff"Tff7b72>
Tff7b72<MaterialDesignIcon
Te6edf3icon-name=Ta5d6ff"Ta5d6ffmagnifyTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffabsolute left-4 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400 group-focus-within:text-blue-500 transition-colorsTa5d6ff"
Tff7b72/>
Tff7b72<input
Te6edf3v-model=Ta5d6ff"Ta5d6ffsearchTermTa5d6ff"
Te6edf3type=Ta5d6ff"Ta5d6fftextTa5d6ff"
Te6edf3:placeholder=Ta5d6ff"Te6edf3$tTa5d6ff(Ta5d6ff'Ta5d6ffinterfaces.search_placeholderTa5d6ff'Ta5d6ff)Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffw-full pl-12 pr-4 py-3 bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 shadow-smTa5d6ff"
Tff7b72/>
Tff7b72<button
Te6edf3v-if=Ta5d6ff"Ta5d6ffsearchTermTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffabsolute inset-y-0 right-0 pr-4 flex items-center text-gray-400 hover:text-gray-600 dark:hover:text-gray-200Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffsearchTerm = Ta5d6ff'Ta5d6ff'Ta5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffclose-circleTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-5Ta5d6ff" Tff7b72/>
Tff7b72</button>
Tff7b72</div>
Tff7b72<divTff7b72>
Tff7b72<select
Te6edf3v-model=Ta5d6ff"Ta5d6fftypeFilterTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffw-full px-4 py-2.5 bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-700 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50 text-gray-900 dark:text-whiteTa5d6ff"
Tff7b72>
Tff7b72<option Te6edf3value=Ta5d6ff"Ta5d6ffallTa5d6ff"Tff7b72>{{ Te6edf3$t("interfaces.all_types") }}Tff7b72</option>
Tff7b72<option Te6edf3v-for=Ta5d6ff"Ta5d6fftype in sortedInterfaceTypesTa5d6ff" Te6edf3:key=Ta5d6ff"Ta5d6fftypeTa5d6ff" Te6edf3:value=Ta5d6ff"Ta5d6fftypeTa5d6ff"Tff7b72>
{{ type }}
Tff7b72</option>
Tff7b72</select>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>

Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffinterfaces-section space-y-4Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex flex-wrap gap-2Ta5d6ff"Tff7b72>
Tff7b72<button
Te6edf3v-for=Ta5d6ff"Ta5d6fftab in [Ta5d6ff'Ta5d6ffoverviewTa5d6ff'Ta5d6ff, Ta5d6ff'Ta5d6ffdiscoveryTa5d6ff'Ta5d6ff]Ta5d6ff"
Te6edf3:key=Ta5d6ff"Ta5d6fftabTa5d6ff"
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3:class=Ta5d6ff"Ta5d6fftabChipClass(activeTab === tab)Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffactiveTab = tabTa5d6ff"
Tff7b72>
Tff7b72<span Te6edf3v-if=Ta5d6ff"Ta5d6fftab === Ta5d6ff'Ta5d6ffoverviewTa5d6ff'Ta5d6ff"Tff7b72>OverviewTff7b72</span>
Tff7b72<span v-elseTff7b72>Discovery SettingsTff7b72</span>
Tff7b72</button>
Tff7b72</div>

Tff7b72<div Te6edf3v-if=Ta5d6ff"Ta5d6ffactiveTab === Ta5d6ff'Ta5d6ffoverviewTa5d6ff'Ta5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffspace-y-4Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffinterfaces-subpanel space-y-3Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex flex-wrap items-center justify-between gap-4Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffspace-y-1Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fftext-xs uppercase tracking-wide text-gray-500 dark:text-gray-400Ta5d6ff"Tff7b72>
Configured
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fftext-xl font-semibold text-gray-900 dark:text-whiteTa5d6ff"Tff7b72>
Interfaces
Tff7b72<span
Te6edf3v-if=Ta5d6ff"Ta5d6fffilteredInterfaces.length > 0Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffml-2 text-sm font-medium text-gray-400Ta5d6ff"
Tff7b72>({{ filteredInterfaces.length }})Tff7b72</span
>
Tff7b72</div>
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex gap-2 flex-wrapTa5d6ff"Tff7b72>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3:class=Ta5d6ff"Ta5d6fffilterChipClass(statusFilter === Ta5d6ff'Ta5d6ffallTa5d6ff'Ta5d6ff)Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ff!py-1 !px-3Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffsetStatusFilter(Ta5d6ff'Ta5d6ffallTa5d6ff'Ta5d6ff)Ta5d6ff"
Tff7b72>
{{ Te6edf3$t("interfaces.all") }}
Tff7b72</button>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3:class=Ta5d6ff"Ta5d6fffilterChipClass(statusFilter === Ta5d6ff'Ta5d6ffenabledTa5d6ff'Ta5d6ff)Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ff!py-1 !px-3Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffsetStatusFilter(Ta5d6ff'Ta5d6ffenabledTa5d6ff'Ta5d6ff)Ta5d6ff"
Tff7b72>
{{ Te6edf3$t("app.enabled") }}
Tff7b72</button>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3:class=Ta5d6ff"Ta5d6fffilterChipClass(statusFilter === Ta5d6ff'Ta5d6ffdisabledTa5d6ff'Ta5d6ff)Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ff!py-1 !px-3Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffsetStatusFilter(Ta5d6ff'Ta5d6ffdisabledTa5d6ff'Ta5d6ff)Ta5d6ff"
Tff7b72>
{{ Te6edf3$t("app.disabled") }}
Tff7b72</button>
Tff7b72</div>
Tff7b72</div>
Tff7b72<div
Te6edf3v-if=Ta5d6ff"Ta5d6fffilteredInterfaces.length !== 0Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffgrid gap-4 xl:grid-cols-2 2xl:grid-cols-3 3xl:grid-cols-4 4xl:grid-cols-5Ta5d6ff"
Tff7b72>
Tff7b72<Interface
Te6edf3v-for=Ta5d6ff"Ta5d6ffiface of filteredInterfacesTa5d6ff"
Te6edf3:key=Ta5d6ff"Ta5d6ffiface._nameTa5d6ff"
Te6edf3:iface=Ta5d6ff"Ta5d6ffifaceTa5d6ff"
Te6edf3:is-reticulum-running=Ta5d6ff"Ta5d6ffisReticulumRunningTa5d6ff"
@Te6edf3enable=Ta5d6ff"Ta5d6ffenableInterface(iface._name)Ta5d6ff"
@Te6edf3disable=Ta5d6ff"Ta5d6ffdisableInterface(iface._name)Ta5d6ff"
@Te6edf3edit=Ta5d6ff"Ta5d6ffeditInterface(iface._name)Ta5d6ff"
@Te6edf3export=Ta5d6ff"Ta5d6ffexportInterface(iface._name)Ta5d6ff"
@Te6edf3delete=Ta5d6ff"Ta5d6ffdeleteInterface(iface._name)Ta5d6ff"
Tff7b72/>
Tff7b72</div>
Tff7b72<div
v-else
Te6edf3class=Ta5d6ff"Ta5d6fftext-center py-10 px-4 text-gray-500 dark:text-gray-300 border border-dashed border-gray-200 dark:border-zinc-800 rounded-xlTa5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6fflan-disconnectTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffw-10 h-10 mx-auto mb-3Ta5d6ff" Tff7b72/>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fftext-lg font-semiboldTa5d6ff"Tff7b72>{{ Te6edf3$t("interfaces.no_interfaces_found") }}Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fftext-smTa5d6ff"Tff7b72>{{ Te6edf3$t("interfaces.no_interfaces_description") }}Tff7b72</div>
Tff7b72</div>
Tff7b72</div>

Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffinterfaces-subpanel space-y-3Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex flex-col gap-3 min-w-0 lg:flex-row lg:items-start lg:justify-betweenTa5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffmin-w-0 flex-1Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fftext-xs uppercase tracking-wide text-gray-500 dark:text-gray-400Ta5d6ff"Tff7b72>
Discovered Interfaces
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fftext-xl font-semibold text-gray-900 dark:text-whiteTa5d6ff"Tff7b72>
Recently Heard Announces
Tff7b72<span
Te6edf3v-if=Ta5d6ff"Ta5d6ffsortedDiscoveredInterfaces.length > 0Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffml-2 text-sm font-medium text-gray-400Ta5d6ff"
Tff7b72>({{ sortedDiscoveredInterfaces.length }})Tff7b72</span
>
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fftext-sm text-gray-600 dark:text-gray-300Ta5d6ff"Tff7b72>
Discovery runs continually; heard announces stay listed. Connected entries show
a green pill; disconnected entries are dimmed with a red label.
Tff7b72</div>
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex flex-wrap gap-2 items-center shrink-0 min-w-0Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex gap-1.5 mr-2Ta5d6ff"Tff7b72>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3:class=Ta5d6ff"Ta5d6fffilterChipClass(discoveredStatusFilter === Ta5d6ff'Ta5d6ffallTa5d6ff'Ta5d6ff)Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ff!py-1 !px-3Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffdiscoveredStatusFilter = Ta5d6ff'Ta5d6ffallTa5d6ff'Ta5d6ff"
Tff7b72>
{{ Te6edf3$t("interfaces.all") }}
Tff7b72</button>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3:class=Ta5d6ff"Ta5d6fffilterChipClass(discoveredStatusFilter === Ta5d6ff'Ta5d6ffconnectedTa5d6ff'Ta5d6ff)Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ff!py-1 !px-3Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffdiscoveredStatusFilter = Ta5d6ff'Ta5d6ffconnectedTa5d6ff'Ta5d6ff"
Tff7b72>
{{ Te6edf3$t("interfaces.connected_only") }}
Tff7b72</button>
Tff7b72</div>
Tff7b72<button
Te6edf3v-if=Ta5d6ff"Ta5d6ffinterfacesWithLocation.length > 0Ta5d6ff"
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffsecondary-chip text-xs bg-blue-500/10 hover:bg-blue-500/20 text-blue-600 dark:text-blue-400 border-blue-500/30Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffmapAllDiscoveredTa5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffmap-marker-multipleTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffw-4 h-4Ta5d6ff" Tff7b72/>
Map All ({{ interfacesWithLocation.length }})
Tff7b72</button>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffsecondary-chip text-xsTa5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffloadDiscoveredInterfacesTa5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffrefreshTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffw-4 h-4Ta5d6ff" Tff7b72/>
Refresh
Tff7b72</button>
Tff7b72</div>
Tff7b72</div>

Tff7b72<div
Te6edf3v-if=Ta5d6ff"Ta5d6ffsortedDiscoveredInterfaces.length === 0Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6fftext-sm text-gray-500 dark:text-gray-300Ta5d6ff"
Tff7b72>
{{ discoveredEmptyMessage }}
Tff7b72</div>

Tff7b72<div
v-else
Te6edf3class=Ta5d6ff"Ta5d6ffgrid gap-4 min-w-0 grid-cols-1 sm:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 3xl:grid-cols-5 4xl:grid-cols-6Ta5d6ff"
Tff7b72>
Tff7b72<div
Te6edf3v-for=Ta5d6ff"Ta5d6ffiface in sortedDiscoveredInterfacesTa5d6ff"
Te6edf3:key=Ta5d6ff"Ta5d6ffiface.discovery_hash || iface.nameTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffinterface-card group transition-all duration-300 min-w-0Ta5d6ff"
Te6edf3:class=Ta5d6ff"Ta5d6ff{
Ta5d6ff'Ta5d6ffopacity-85 md:opacity-70 md:grayscale-[0.3]Ta5d6ff'Ta5d6ff: !isDiscoveredConnected(iface),
}Ta5d6ff"
Tff7b72>
Tff7b72<div
Te6edf3class=Ta5d6ff"Ta5d6ffflex flex-col gap-3 sm:flex-row sm:gap-4 sm:items-start relative min-w-0Ta5d6ff"
Tff7b72>
T8b949e<!-- Disconnected Overlay -->
Tff7b72<div
Te6edf3v-if=Ta5d6ff"Ta5d6ff!isDiscoveredConnected(iface)Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffabsolute inset-0 z-10 flex items-center justify-center bg-white/25 dark:bg-zinc-900/25 md:backdrop-blur-[0.5px] rounded-3xl pointer-events-noneTa5d6ff"
Tff7b72>
Tff7b72<div
Te6edf3class=Ta5d6ff"Ta5d6ffbg-red-500/90 text-white px-3 py-1.5 rounded-full shadow-lg flex items-center gap-2 text-[10px] font-bold uppercase tracking-widerTa5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon
Te6edf3:icon-name=Ta5d6ff"
Ta5d6ff isDiscoveredBlacklisted(iface) ? Ta5d6ff'Ta5d6ffcancelTa5d6ff'Ta5d6ff : Ta5d6ff'Ta5d6fflan-disconnectTa5d6ff'
Ta5d6ff Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffw-3.5 h-3.5Ta5d6ff"
Tff7b72/>
Tff7b72<spanTff7b72>{{
isDiscoveredBlacklisted(iface) ? "Blacklisted" : Te6edf3$t("app.disabled")
}}Tff7b72</span>
Tff7b72</div>
Tff7b72</div>

Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffinterface-card__icon shrink-0Ta5d6ff"Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3:icon-name=Ta5d6ff"Ta5d6ffgetDiscoveryIcon(iface)Ta5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffw-6 h-6Ta5d6ff" Tff7b72/>
Tff7b72</div>

Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex-1 min-w-0 space-y-2Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex items-center gap-2 flex-nowrap min-w-0Ta5d6ff"Tff7b72>
Tff7b72<div
Te6edf3class=Ta5d6ff"Ta5d6fftext-base sm:text-lg font-semibold text-gray-900 dark:text-white truncate min-w-0Ta5d6ff"
Tff7b72>
{{ iface.name }}
Tff7b72</div>
Tff7b72<span Te6edf3class=Ta5d6ff"Ta5d6fftype-chip shrink-0Ta5d6ff"Tff7b72>{{ iface.type }}Tff7b72</span>
Tff7b72</div>

Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex items-center gap-2 flex-wrapTa5d6ff"Tff7b72>
Tff7b72<span
Te6edf3v-if=Ta5d6ff"Ta5d6ffiface.valueTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6fftext-[10px] font-bold text-blue-600 dark:text-blue-400Ta5d6ff"
Tff7b72>
Stamps: {{ iface.value }}
Tff7b72</span>
Tff7b72<span
Te6edf3v-if=Ta5d6ff"Ta5d6ffisDiscoveredConnected(iface)Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffinline-flex items-center rounded-full bg-emerald-100 text-emerald-700 px-2 py-0.5 text-[10px] font-semibold dark:bg-emerald-900/40 dark:text-emerald-200 shrink-0Ta5d6ff"
Tff7b72>
Connected
Tff7b72</span>
Tff7b72</div>

Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex flex-wrap gap-1.5 text-[10px] sm:text-xsTa5d6ff"Tff7b72>
Tff7b72<span Te6edf3class=Ta5d6ff"Ta5d6ffstat-chip bg-gray-50 dark:bg-zinc-800/50Ta5d6ff"
Tff7b72>Hops: {{ iface.hops }}Tff7b72</span
>
Tff7b72<span Te6edf3class=Ta5d6ff"Ta5d6ffstat-chip capitalize bg-gray-50 dark:bg-zinc-800/50Ta5d6ff"Tff7b72>{{
iface.status
}}Tff7b72</span>
Tff7b72<span
Te6edf3v-if=Ta5d6ff"Ta5d6ffiface.last_heardTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffstat-chip bg-gray-50 dark:bg-zinc-800/50Ta5d6ff"
Tff7b72>
Heard: {{ formatLastHeard(iface.last_heard) }}
Tff7b72</span>
Tff7b72</div>

Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffgrid gap-1.5 text-[10px] sm:text-[11px] pt-1 min-w-0Ta5d6ff"Tff7b72>
Tff7b72<div
Te6edf3v-if=Ta5d6ff"Ta5d6ffiface.reachable_onTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffflex items-center gap-2 text-gray-600 dark:text-gray-400 hover:text-blue-500 cursor-pointer transition-colors min-w-0Ta5d6ff"
@Te6edf3click=Ta5d6ff"
Ta5d6ff copyToClipboard(
`Tff7b72${Te6edf3ifaceTff7b72.Td2a8ffreachable_onTff7b72}Ta5d6ff:Tff7b72${Te6edf3ifaceTff7b72.Td2a8ffportTff7b72}Ta5d6ff`,
Ta5d6ff'Ta5d6ffAddressTa5d6ff'
Ta5d6ff )
Ta5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon
Te6edf3icon-name=Ta5d6ff"Ta5d6fflink-variantTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffw-3.5 h-3.5 shrink-0Ta5d6ff"
Tff7b72/>
Tff7b72<span Te6edf3class=Ta5d6ff"Ta5d6fftruncateTa5d6ff"
Tff7b72>Address: {{ iface.reachable_on }}:{{ iface.port }}Tff7b72</span
>
Tff7b72</div>

Tff7b72<div
Te6edf3v-if=Ta5d6ff"Ta5d6ffiface.transport_idTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffflex items-center gap-2 text-gray-600 dark:text-gray-400 hover:text-blue-500 cursor-pointer transition-colors min-w-0Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffcopyToClipboard(iface.transport_id, Ta5d6ff'Ta5d6ffTransport IDTa5d6ff'Ta5d6ff)Ta5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon
Te6edf3icon-name=Ta5d6ff"Ta5d6ffidentifierTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffw-3.5 h-3.5 shrink-0Ta5d6ff"
Tff7b72/>
Tff7b72<span Te6edf3class=Ta5d6ff"Ta5d6fftruncate font-monoTa5d6ff"
Tff7b72>Transport ID: {{ iface.transport_id }}Tff7b72</span
>
Tff7b72</div>

Tff7b72<div
Te6edf3v-if=Ta5d6ff"Ta5d6ffiface.network_idTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffflex items-center gap-2 text-gray-600 dark:text-gray-400 hover:text-blue-500 cursor-pointer transition-colors min-w-0Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffcopyToClipboard(iface.network_id, Ta5d6ff'Ta5d6ffNetwork IDTa5d6ff'Ta5d6ff)Ta5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6fflanTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffw-3.5 h-3.5 shrink-0Ta5d6ff" Tff7b72/>
Tff7b72<span Te6edf3class=Ta5d6ff"Ta5d6fftruncate font-monoTa5d6ff"
Tff7b72>Network ID: {{ iface.network_id }}Tff7b72</span
>
Tff7b72</div>

Tff7b72<div
Te6edf3v-if=Ta5d6ff"Ta5d6ffdiscoveredNetworkName(iface)Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffflex items-center gap-2 text-amber-700 dark:text-amber-300 hover:text-amber-500 cursor-pointer transition-colors min-w-0Ta5d6ff"
Te6edf3:title=Ta5d6ff"Te6edf3$tTa5d6ff(Ta5d6ff'Ta5d6ffinterfaces.discovered_copy_network_nameTa5d6ff'Ta5d6ff)Ta5d6ff"
Te6edf3data-testid=Ta5d6ff"Ta5d6ffdiscovered-network-nameTa5d6ff"
@Te6edf3click=Ta5d6ff"
Ta5d6ff copyToClipboard(
discoveredNetworkName(iface),
Te6edf3$tTa5d6ff(Ta5d6ff'Ta5d6ffinterfaces.discovered_network_nameTa5d6ff'Ta5d6ff)
)
Ta5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon
Te6edf3icon-name=Ta5d6ff"Ta5d6ffshield-keyTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffw-3.5 h-3.5 shrink-0Ta5d6ff"
Tff7b72/>
Tff7b72<span Te6edf3class=Ta5d6ff"Ta5d6fftruncate font-monoTa5d6ff"
Tff7b72>{{ Te6edf3$t("interfaces.discovered_network_name") }}:
{{ discoveredNetworkName(iface) }}Tff7b72</span
>
Tff7b72</div>

Tff7b72<div
Te6edf3v-if=Ta5d6ff"Ta5d6ffdiscoveredPassphrase(iface)Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffflex items-center gap-2 text-amber-700 dark:text-amber-300 hover:text-amber-500 cursor-pointer transition-colors min-w-0Ta5d6ff"
Te6edf3:title=Ta5d6ff"Te6edf3$tTa5d6ff(Ta5d6ff'Ta5d6ffinterfaces.discovered_copy_passphraseTa5d6ff'Ta5d6ff)Ta5d6ff"
Te6edf3data-testid=Ta5d6ff"Ta5d6ffdiscovered-passphraseTa5d6ff"
@Te6edf3click=Ta5d6ff"
Ta5d6ff copyToClipboard(
discoveredPassphrase(iface),
Te6edf3$tTa5d6ff(Ta5d6ff'Ta5d6ffinterfaces.discovered_passphraseTa5d6ff'Ta5d6ff)
)
Ta5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon
Te6edf3icon-name=Ta5d6ff"Ta5d6ffshield-lockTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffw-3.5 h-3.5 shrink-0Ta5d6ff"
Tff7b72/>
Tff7b72<span Te6edf3class=Ta5d6ff"Ta5d6fftruncate font-monoTa5d6ff"
Tff7b72>{{ Te6edf3$t("interfaces.discovered_passphrase") }}:
{{ maskPassphrase(discoveredPassphrase(iface)) }}Tff7b72</span
>
Tff7b72</div>

Tff7b72<div
Te6edf3v-if=Ta5d6ff"Ta5d6ffiface.latitude != null && iface.longitude != nullTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffflex items-center gap-2 text-gray-600 dark:text-gray-400 hover:text-blue-500 cursor-pointer transition-colors min-w-0Ta5d6ff"
@Te6edf3click=Ta5d6ff"
Ta5d6ff copyToClipboard(
`Tff7b72${Te6edf3ifaceTff7b72.Td2a8fflatitudeTff7b72}Ta5d6ff, Tff7b72${Te6edf3ifaceTff7b72.Td2a8fflongitudeTff7b72}Ta5d6ff`,
Ta5d6ff'Ta5d6ffLocationTa5d6ff'
Ta5d6ff )
Ta5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon
Te6edf3icon-name=Ta5d6ff"Ta5d6ffmap-markerTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffw-3.5 h-3.5 shrink-0Ta5d6ff"
Tff7b72/>
Tff7b72<span Te6edf3class=Ta5d6ff"Ta5d6fftruncateTa5d6ff"
Tff7b72>Loc: {{ iface.latitude }}, {{ iface.longitude }}Tff7b72</span
>
Tff7b72</div>

Tff7b72<div
Te6edf3v-if=Ta5d6ff"Ta5d6ffdiscoveredBytes(iface)Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffflex items-center gap-2 text-gray-500 dark:text-gray-500 min-w-0Ta5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon
Te6edf3icon-name=Ta5d6ff"Ta5d6ffswap-verticalTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffw-3.5 h-3.5 shrink-0Ta5d6ff"
Tff7b72/>
Tff7b72<span Te6edf3class=Ta5d6ff"Ta5d6fftruncateTa5d6ff"
Tff7b72>TX {{ discoveredBytes(iface).tx }} · RX
{{ discoveredBytes(iface).rx }}Tff7b72</span
>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>

Tff7b72<div
Te6edf3class=Ta5d6ff"Ta5d6ffflex flex-row sm:flex-col gap-2 shrink-0 self-end sm:self-auto justify-endTa5d6ff"
Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffrelativeTa5d6ff"Tff7b72>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffsecondary-chip !p-2 !rounded-xlTa5d6ff"
Te6edf3title=Ta5d6ff"Ta5d6ffDiscovery actionsTa5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6fftoggleDiscoveryActionsMenu(iface)Ta5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffdots-verticalTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffw-4 h-4Ta5d6ff" Tff7b72/>
Tff7b72</button>
Tff7b72<div
Te6edf3v-if=Ta5d6ff"Ta5d6ffopenDiscoveryActionKey === discoveryKey(iface)Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffabsolute right-0 mt-1 z-20 min-w-44 rounded-xl border border-gray-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 shadow-lg p-1Ta5d6ff"
Tff7b72>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffw-full text-left px-3 py-2 text-xs rounded-lg hover:bg-blue-50 dark:hover:bg-blue-900/20 text-blue-700 dark:text-blue-300Ta5d6ff"
Te6edf3data-testid=Ta5d6ff"Ta5d6ffuse-discovered-interfaceTa5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffuseDiscoveredInterface(iface)Ta5d6ff"
Tff7b72>
{{ Te6edf3$t("interfaces.discovered_use_this") }}
Tff7b72</button>
Tff7b72<button
Te6edf3v-if=Ta5d6ff"Ta5d6ffiface.config_entryTa5d6ff"
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffw-full text-left px-3 py-2 text-xs rounded-lg hover:bg-gray-50 dark:hover:bg-zinc-800 text-gray-700 dark:text-gray-200Ta5d6ff"
Te6edf3data-testid=Ta5d6ff"Ta5d6ffcopy-discovered-configTa5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffcopyDiscoveredConfigEntry(iface)Ta5d6ff"
Tff7b72>
{{ Te6edf3$t("interfaces.discovered_copy_config") }}
Tff7b72</button>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffw-full text-left px-3 py-2 text-xs rounded-lg hover:bg-emerald-50 dark:hover:bg-emerald-900/20 text-emerald-700 dark:text-emerald-300Ta5d6ff"
Te6edf3:disabled=Ta5d6ff"Ta5d6ffsavingDiscoveryActionTa5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffaddDiscoveredInterfaceToList(iface, Ta5d6ff'Ta5d6ffallowTa5d6ff'Ta5d6ff)Ta5d6ff"
Tff7b72>
Allow this announce
Tff7b72</button>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffw-full text-left px-3 py-2 text-xs rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20 text-red-700 dark:text-red-300Ta5d6ff"
Te6edf3:disabled=Ta5d6ff"Ta5d6ffsavingDiscoveryActionTa5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffaddDiscoveredInterfaceToList(iface, Ta5d6ff'Ta5d6ffblockTa5d6ff'Ta5d6ff)Ta5d6ff"
Tff7b72>
Blacklist this announce
Tff7b72</button>
Tff7b72</div>
Tff7b72</div>
Tff7b72<button
Te6edf3v-if=Ta5d6ff"Ta5d6ffiface.latitude != null && iface.longitude != nullTa5d6ff"
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffsecondary-chip !p-2 !rounded-xlTa5d6ff"
Te6edf3:title=Ta5d6ff"Te6edf3$tTa5d6ff(Ta5d6ff'Ta5d6ffmap.titleTa5d6ff'Ta5d6ff)Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffgoToMap(iface)Ta5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffmapTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffw-4 h-4Ta5d6ff" Tff7b72/>
Tff7b72</button>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>

Tff7b72<div v-else Te6edf3class=Ta5d6ff"Ta5d6ffspace-y-4Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffinterfaces-subpanel space-y-4Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex flex-wrap gap-3 items-centerTa5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex-1Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fftext-xs uppercase tracking-wide text-gray-500 dark:text-gray-400Ta5d6ff"Tff7b72>
Discovery
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fftext-xl font-semibold text-gray-900 dark:text-whiteTa5d6ff"Tff7b72>
Interface Discovery
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fftext-sm text-gray-600 dark:text-gray-300Ta5d6ff"Tff7b72>
Publish your interfaces for others to find, or listen for announced entrypoints
and auto-connect to them.
Tff7b72</div>
Tff7b72</div>
Tff7b72<RouterLink Te6edf3:to=Ta5d6ff"Ta5d6ff{ name: Ta5d6ff'Ta5d6ffinterfaces.addTa5d6ff'Ta5d6ff }Ta5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsecondary-chip text-smTa5d6ff"Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6fflanTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffw-4 h-4Ta5d6ff" Tff7b72/>
Configure Per-Interface
Tff7b72</RouterLink>
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffgrid gap-4 min-w-0 lg:grid-cols-2Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffspace-y-2 text-sm text-gray-700 dark:text-gray-300 min-w-0Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fffont-semibold text-gray-900 dark:text-whiteTa5d6ff"Tff7b72>Publish (Server)Tff7b72</div>
Tff7b72<divTff7b72>
Enable discovery while adding or editing an interface to broadcast reachable
details. Reticulum will sign and stamp announces automatically.
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fftext-xs text-gray-500 dark:text-gray-400Ta5d6ff"Tff7b72>
Requires LXMF in the Python environment. Transport is optional for publishing,
but usually recommended so peers can connect back.
Tff7b72</div>
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffspace-y-3 min-w-0Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex flex-col gap-3 sm:flex-row sm:items-center sm:justify-betweenTa5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex flex-col min-w-0 pr-0 sm:pr-4Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fftext-sm font-semibold text-gray-900 dark:text-whiteTa5d6ff"Tff7b72>
Discover Interfaces (Peer)
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fftext-xs text-gray-500 dark:text-gray-400Ta5d6ff"Tff7b72>
Listen for discovery announces and optionally auto-connect to available
interfaces.
Tff7b72</div>
Tff7b72</div>
Tff7b72<Toggle
Te6edf3v-model=Ta5d6ff"Ta5d6ffdiscoveryConfig.discover_interfacesTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffshrink-0 sm:my-autoTa5d6ff"
Tff7b72/>
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffgrid grid-cols-1 gap-2 sm:grid-cols-2 min-w-0Ta5d6ff"Tff7b72>
Tff7b72<divTff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fftext-xs font-semibold text-gray-700 dark:text-gray-200Ta5d6ff"Tff7b72>
Allowed Sources
Tff7b72</div>
Tff7b72<input
Te6edf3v-model=Ta5d6ff"Ta5d6ffdiscoveryConfig.interface_discovery_sourcesTa5d6ff"
Te6edf3type=Ta5d6ff"Ta5d6fftextTa5d6ff"
Te6edf3placeholder=Ta5d6ff"Ta5d6ffComma separated identity hashesTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffinput-fieldTa5d6ff"
Tff7b72/>
Tff7b72</div>
Tff7b72<divTff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fftext-xs font-semibold text-gray-700 dark:text-gray-200Ta5d6ff"Tff7b72>
Discovery Whitelist
Tff7b72</div>
Tff7b72<input
Te6edf3v-model=Ta5d6ff"Ta5d6ffdiscoveryConfig.interface_discovery_whitelistTa5d6ff"
Te6edf3type=Ta5d6ff"Ta5d6fftextTa5d6ff"
Te6edf3placeholder=Ta5d6ff"Ta5d6ffNames, hosts, ids, or host:port globsTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffinput-fieldTa5d6ff"
Tff7b72/>
Tff7b72</div>
Tff7b72<divTff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fftext-xs font-semibold text-gray-700 dark:text-gray-200Ta5d6ff"Tff7b72>
Discovery Blacklist
Tff7b72</div>
Tff7b72<input
Te6edf3v-model=Ta5d6ff"Ta5d6ffdiscoveryConfig.interface_discovery_blacklistTa5d6ff"
Te6edf3type=Ta5d6ff"Ta5d6fftextTa5d6ff"
Te6edf3placeholder=Ta5d6ff"Ta5d6ffNames, hosts, ids, or host:port globsTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffinput-fieldTa5d6ff"
Tff7b72/>
Tff7b72</div>
Tff7b72<divTff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fftext-xs font-semibold text-gray-700 dark:text-gray-200Ta5d6ff"Tff7b72>
Required Stamp Value
Tff7b72</div>
Tff7b72<input
v-model.Te6edf3number=Ta5d6ff"Ta5d6ffdiscoveryConfig.required_discovery_valueTa5d6ff"
Te6edf3type=Ta5d6ff"Ta5d6ffnumberTa5d6ff"
Te6edf3min=Ta5d6ff"Ta5d6ff0Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffinput-fieldTa5d6ff"
Tff7b72/>
Tff7b72</div>
Tff7b72<divTff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fftext-xs font-semibold text-gray-700 dark:text-gray-200Ta5d6ff"Tff7b72>
Auto-connect Slots
Tff7b72</div>
Tff7b72<input
v-model.Te6edf3number=Ta5d6ff"Ta5d6ffdiscoveryConfig.autoconnect_discovered_interfacesTa5d6ff"
Te6edf3type=Ta5d6ff"Ta5d6ffnumberTa5d6ff"
Te6edf3min=Ta5d6ff"Ta5d6ff0Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffinput-fieldTa5d6ff"
Tff7b72/>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fftext-xs text-gray-500 dark:text-gray-400Ta5d6ff"Tff7b72>
0 disables auto-connect.
Tff7b72</div>
Tff7b72</div>
Tff7b72<divTff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fftext-xs font-semibold text-gray-700 dark:text-gray-200Ta5d6ff"Tff7b72>
Network Identity Path
Tff7b72</div>
Tff7b72<input
Te6edf3v-model=Ta5d6ff"Ta5d6ffdiscoveryConfig.network_identityTa5d6ff"
Te6edf3type=Ta5d6ff"Ta5d6fftextTa5d6ff"
Te6edf3placeholder=Ta5d6ff"Ta5d6ff~/.reticulum/storage/identities/...Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffinput-fieldTa5d6ff"
Tff7b72/>
Tff7b72</div>
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex justify-endTa5d6ff"Tff7b72>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffprimary-chip text-xsTa5d6ff"
Te6edf3:disabled=Ta5d6ff"Ta5d6ffsavingDiscoveryTa5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffsaveDiscoveryConfigTa5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon
Te6edf3:icon-name=Ta5d6ff"Ta5d6ffsavingDiscovery ? Ta5d6ff'Ta5d6ffprogress-clockTa5d6ff'Ta5d6ff : Ta5d6ff'Ta5d6ffcontent-saveTa5d6ff'Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffw-4 h-4Ta5d6ff"
Te6edf3:class=Ta5d6ff"Ta5d6ff{ Ta5d6ff'Ta5d6ffanimate-spin-reverseTa5d6ff'Ta5d6ff: savingDiscovery }Ta5d6ff"
Tff7b72/>
Tff7b72<span Te6edf3class=Ta5d6ff"Ta5d6ffml-1Ta5d6ff"Tff7b72>Save Discovery SettingsTff7b72</span>
Tff7b72</button>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>

Tff7b72<RouterLink
Te6edf3:to=Ta5d6ff"Ta5d6ff{ name: Ta5d6ff'Ta5d6ffinterfaces.addTa5d6ff'Ta5d6ff }Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffsm:hidden fixed bottom-5 right-4 z-[60] flex h-14 w-14 items-center justify-center rounded-full bg-blue-600 text-white shadow-lg ring-1 ring-blue-400/30 transition active:scale-95Ta5d6ff"
Te6edf3:title=Ta5d6ff"Te6edf3$tTa5d6ff(Ta5d6ff'Ta5d6ffinterfaces.add_interfaceTa5d6ff'Ta5d6ff)Ta5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffplusTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffw-7 h-7Ta5d6ff" Tff7b72/>
Tff7b72</RouterLink>

Tff7b72<ImportInterfacesModal Te6edf3ref=Ta5d6ff"Ta5d6ffimport-interfaces-modalTa5d6ff" @Te6edf3dismissed=Ta5d6ff"Ta5d6ffonImportInterfacesModalDismissedTa5d6ff" Tff7b72/>
Tff7b72</template>

Tff7b72<scriptTff7b72>
import DialogUtils from "../../js/DialogUtils";
import ElectronUtils from "../../js/ElectronUtils";
import Interface from "./Interface.vue";
import Utils from "../../js/Utils";
import ImportInterfacesModal from "./ImportInterfacesModal.vue";
import DownloadUtils from "../../js/DownloadUtils";
import MaterialDesignIcon from "../MaterialDesignIcon.vue";
import ToastUtils from "../../js/ToastUtils";
import GlobalState from "../../js/GlobalState";
import Toggle from "../forms/Toggle.vue";

export default {
name: "InterfacesPage",
components: {
Toggle,
ImportInterfacesModal,
Interface,
MaterialDesignIcon,
},
data() {
return {
interfaces: {},
interfaceStats: {},
reloadInterval: null,
searchTerm: "",
statusFilter: "all",
typeFilter: "all",
reloadingRns: false,
isReticulumRunning: true,
discoveryConfig: {
discover_interfaces: false,
interface_discovery_sources: "",
interface_discovery_whitelist: "",
interface_discovery_blacklist: "",
required_discovery_value: null,
autoconnect_discovered_interfaces: 0,
network_identity: "",
},
savingDiscovery: false,
savingDiscoveryAction: false,
openDiscoveryActionKey: null,
discoveredInterfaces: [],
discoveredActive: [],
discoveredStatusFilter: "all",
discoveryInterval: null,
activeTab: "overview",
};
},
computed: {
hasPendingInterfaceChanges() {
return GlobalState.hasPendingInterfaceChanges;
},
modifiedInterfaceNames() {
return GlobalState.modifiedInterfaceNames;
},
isElectron() {
return ElectronUtils.isElectron();
},
showRestartReminder() {
return this.hasPendingInterfaceChanges;
},
interfacesWithStats() {
const results = [];
for (const [interfaceName, iface] of Object.entries(this.interfaces)) {
iface._name = interfaceName;
iface._stats = this.interfaceStats[interfaceName];
iface._restart_required = this.modifiedInterfaceNames.has(interfaceName);
results.push(iface);
}
return results;
},
enabledInterfaces() {
return this.interfacesWithStats.filter((iface) => this.isInterfaceEnabled(iface));
},
disabledInterfaces() {
return this.interfacesWithStats.filter((iface) => !this.isInterfaceEnabled(iface));
},
filteredInterfaces() {
const search = this.searchTerm.toLowerCase().trim();
return this.interfacesWithStats
.filter((iface) => {
if (this.statusFilter === "enabled" && !this.isInterfaceEnabled(iface)) {
return false;
}
if (this.statusFilter === "disabled" && this.isInterfaceEnabled(iface)) {
return false;
}
if (this.typeFilter !== "all" && iface.type !== this.typeFilter) {
return false;
}
if (!search) {
return true;
}
const haystack = [
iface._name,
iface.type,
iface.target_host,
iface.target_port,
iface.listen_ip,
iface.listen_port,
]
.filter(Boolean)
.join(" ")
.toLowerCase();
return haystack.includes(search);
})
.sort((a, b) => {
const enabledDiff = Number(this.isInterfaceEnabled(b)) - Number(this.isInterfaceEnabled(a));
if (enabledDiff !== 0) return enabledDiff;
return a._name.localeCompare(b._name);
});
},
sortedInterfaceTypes() {
const types = new Set();
this.interfacesWithStats.forEach((iface) => types.add(iface.type));
return Array.from(types).sort();
},
sortedDiscoveredInterfaces() {
const search = this.searchTerm.toLowerCase().trim();
let list = [...this.discoveredInterfaces];
if (this.discoveredStatusFilter === "connected") {
list = list.filter((iface) => this.isDiscoveredConnected(iface));
}
if (this.typeFilter !== "all") {
list = list.filter((iface) => iface.type === this.typeFilter);
}
if (search) {
list = list.filter((iface) => {
const haystack = [
iface.name,
iface.type,
iface.reachable_on,
iface.port,
iface.transport_id,
iface.network_id,
]
.filter(Boolean)
.join(" ")
.toLowerCase();
return haystack.includes(search);
});
}
return list.sort((a, b) => (b.last_heard || 0) - (a.last_heard || 0));
},
interfacesWithLocation() {
return this.discoveredInterfaces.filter((iface) => iface.latitude != null && iface.longitude != null);
},
activeInterfaceStats() {
return Object.values(this.interfaceStats || {});
},
tabChipClass() {
return (isActive) => (isActive ? "primary-chip text-xs" : "secondary-chip text-xs");
},
discoveredActiveSet() {
const set = new Set();
this.discoveredActive.forEach((a) => {
const host = a.target_host || a.remote || a.listen_ip;
const port = a.target_port || a.listen_port;
if (host && port) {
set.add(`${host}:${port}`);
}
});
return set;
},
discoveredActiveTransportIds() {
const set = new Set();
this.discoveredActive.forEach((a) => {
if (a.transport_id) {
set.add(a.transport_id);
}
});
return set;
},
discoveredEmptyMessage() {
if (!this.isReticulumRunning) {
return "LXMF/Reticulum is not running; discovery cannot listen for announces.";
}
if (!this.discoveryConfig.discover_interfaces) {
return "Discovery is disabled. Enable it to start listening for announces.";
}
return "Discovery is working, be patient while it waits for announces.";
},
discoveryAutoconnectMetadataPresent() {
const fromActive = (this.discoveredActive || []).some(
(a) => a.autoconnect_source != null && a.autoconnect_source !== undefined
);
const fromStats = (this.activeInterfaceStats || []).some(
(s) => s.autoconnect_source != null && s.autoconnect_source !== undefined
);
return fromActive || fromStats;
},
},
beforeUnmount() {
clearInterval(this.reloadInterval);
clearInterval(this.discoveryInterval);
},
mounted() {
this.loadInterfaces();
this.updateInterfaceStats();
this.loadDiscoveryConfig();
this.loadDiscoveredInterfaces();

// update info every few seconds
this.reloadInterval = setInterval(() => {
this.updateInterfaceStats();
}, 1000);

this.discoveryInterval = setInterval(() => {
this.loadDiscoveredInterfaces();
}, 5000);
},
methods: {
relaunch() {
ElectronUtils.relaunch();
},
trackInterfaceChange(interfaceName = null) {
GlobalState.hasPendingInterfaceChanges = true;
if (interfaceName) {
GlobalState.modifiedInterfaceNames.add(interfaceName);
}
},
isInterfaceEnabled: function (iface) {
return Utils.isInterfaceEnabled(iface);
},
async loadInterfaces() {
try {
const response = await window.api.get(`/api/v1/reticulum/interfaces`);
this.interfaces = response.data.interfaces;

// also check app info for running state
const appInfoResponse = await window.api.get(`/api/v1/app/info`);
this.isReticulumRunning = appInfoResponse.data.app_info.is_reticulum_running;
} catch {
// do nothing if failed to load interfaces
}
},
async updateInterfaceStats() {
try {
// fetch interface stats
const response = await window.api.get(`/api/v1/interface-stats`);

// update data
const interfaces = response.data.interface_stats?.interfaces ?? [];
for (const iface of interfaces) {
this.interfaceStats[iface.short_name] = iface;
}
} catch {
// do nothing if failed to load interfaces
}
},
async enableInterface(interfaceName) {
// enable interface
try {
await window.api.post(`/api/v1/reticulum/interfaces/enable`, {
name: interfaceName,
});
this.trackInterfaceChange(interfaceName);
} catch (e) {
DialogUtils.alert(this.$t("interfaces.failed_enable"));
console.log(e);
}

// reload interfaces
await this.loadInterfaces();
},
async disableInterface(interfaceName) {
// disable interface
try {
await window.api.post(`/api/v1/reticulum/interfaces/disable`, {
name: interfaceName,
});
this.trackInterfaceChange(interfaceName);
} catch (e) {
DialogUtils.alert(this.$t("interfaces.failed_disable"));
console.log(e);
}

// reload interfaces
await this.loadInterfaces();
},
async editInterface(interfaceName) {
this.$router.push({
name: "interfaces.edit",
query: {
interface_name: interfaceName,
},
});
},
async deleteInterface(interfaceName) {
// ask user to confirm deleting conversation history
if (!(await DialogUtils.confirm(this.$t("interfaces.delete_confirm")))) {
return;
}

// delete interface
try {
await window.api.post(`/api/v1/reticulum/interfaces/delete`, {
name: interfaceName,
});
this.trackInterfaceChange(interfaceName);
} catch (e) {
DialogUtils.alert(this.$t("interfaces.failed_delete"));
console.log(e);
}

// reload interfaces
await this.loadInterfaces();
},
async exportInterfaces() {
try {
// fetch exported interfaces
const response = await window.api.post("/api/v1/reticulum/interfaces/export");

// download file to browser
DownloadUtils.downloadFile("meshchat_interfaces.txt", new Blob([response.data]));
} catch (e) {
DialogUtils.alert(this.$t("interfaces.failed_export_all"));
console.error(e);
}
},
async exportInterface(interfaceName) {
try {
// fetch exported interfaces
const response = await window.api.post("/api/v1/reticulum/interfaces/export", {
selected_interface_names: [interfaceName],
});

// download file to browser
DownloadUtils.downloadFile(`${interfaceName}.txt`, new Blob([response.data]));
} catch (e) {
DialogUtils.alert(this.$t("interfaces.failed_export_single"));
console.error(e);
}
},
showImportInterfacesModal() {
this.$refs["import-interfaces-modal"].show();
},
onImportInterfacesModalDismissed(imported = false) {
// reload interfaces as something may have been imported
this.loadInterfaces();
if (imported) {
this.trackInterfaceChange();
}
},
async loadDiscoveredInterfaces() {
try {
const response = await window.api.get(`/api/v1/reticulum/discovered-interfaces`);
const incoming = response.data?.interfaces ?? [];
const active = response.data?.active ?? [];

const merged = new Map();
const addOrUpdate = (iface, isNew = false) => {
const key = this.discoveryKey(iface);
const existing =
merged.get(key) || this.discoveredInterfaces.find((i) => this.discoveryKey(i) === key);
const lastHeard = iface.last_heard ?? existing?.last_heard ?? Math.floor(Date.now() / 1000);
merged.set(key, {
...existing,
...iface,
last_heard: lastHeard,
__isNew: isNew || existing?.__isNew,
});
};

this.discoveredInterfaces.forEach((iface) => addOrUpdate(iface, false));
incoming.forEach((iface) => addOrUpdate(iface, true));

this.discoveredInterfaces = Array.from(merged.values());
this.discoveredActive = active;
} catch (e) {
console.log(e);
}
},
discoveryKey(iface) {
return (
iface.discovery_hash ||
`${iface.reachable_on || iface.target_host || iface.remote || iface.listen_ip || iface.name || "unknown"}:${
iface.port || iface.target_port || iface.listen_port || ""
}`
);
},
formatLastHeard(ts) {
const seconds = Math.max(0, Math.floor(Date.now() / 1000 - ts));
if (seconds Tff7b72< 60) return `${seconds}s ago`;
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
return `${Math.floor(seconds / 86400)}d ago`;
},
isDiscoveredConnected(iface) {
const Te6edf3reach = Ta5d6ffiface.reachable_on;
const Te6edf3port = Ta5d6ffiface.port;
const Te6edf3nid = Ta5d6ffiface.network_id ? String(iface.network_id).toLowerCase() : null;
if (iface.transport_id && this.discoveredActiveTransportIds.has(iface.transport_id)) {
return true;
}
const Te6edf3hasMeta = Ta5d6ffthis.discoveryAutoconnectMetadataPresent;
for (const a of this.discoveredActive || []) {
const Te6edf3host = Ta5d6ffa.target_host || a.remote || a.listen_ip;
const Te6edf3p = Ta5d6ffa.target_port || a.listen_port;
if (!host || Te6edf3p =Ta5d6ff= null || !reach || Te6edf3port =Ta5d6ff= null) continue;
if (String(host) !== String(reach) || Number(p) !== Number(port)) continue;
const Te6edf3asrc = Ta5d6ffa.autoconnect_source;
if (asrc != null && asrc !== undefined) {
if (nid !== null && String(asrc).toLowerCase() !== nid) continue;
return true;
}
if (!hasMeta) return true;
}
return this.activeInterfaceStats.some((s) =Tff7b72> {
const hostMatch =
(s.target_host && reach && s.target_host === reach) || (s.remote && reach && s.remote === reach);
const portMatch =
(s.target_port && port && Number(s.target_port) === Number(port)) ||
(s.listen_port && port && Number(s.listen_port) === Number(port));
if (!hostMatch || !portMatch || !(s.connected || s.online)) return false;
const asrc = s.autoconnect_source;
if (asrc != null && asrc !== undefined) {
if (nid !== null) return String(asrc).toLowerCase() === nid;
return true;
}
return !hasMeta;
});
},
goToMap(iface) {
if (iface.latitude == null || iface.longitude == null) return;
this.$router.push({
name: "map",
query: {
lat: iface.latitude,
lon: iface.longitude,
label: iface.name,
},
});
},
mapAllDiscovered() {
this.$router.push({
name: "map",
query: { view: "discovered" },
});
},
discoveredBytes(iface) {
const reach = iface.reachable_on;
const port = iface.port;
const nid = iface.network_id ? String(iface.network_id).toLowerCase() : null;
const stats = this.activeInterfaceStats || [];
const hasMeta = this.discoveryAutoconnectMetadataPresent;
const match = stats.find((s) => {
const host = s.target_host || s.remote || s.interface_name;
const p = s.target_port || s.listen_port;
const hostMatch = host && reach && host === reach;
const portMatch = p && port && Number(p) === Number(port);
if (!hostMatch || !portMatch || !(s.connected || s.online)) return false;
const asrc = s.autoconnect_source;
if (asrc != null && asrc !== undefined) {
if (nid !== null) return String(asrc).toLowerCase() === nid;
return true;
}
return !hasMeta;
});
if (!match) return null;
return {
tx: this.formatBytes(match.txb || 0),
rx: this.formatBytes(match.rxb || 0),
};
},
formatBytes(bytes) {
return Utils.formatBytes(bytes || 0);
},
parseBool(value) {
if (typeof value === "string") {
return ["true", "yes", "1", "y", "on"].includes(value.toLowerCase());
}
return Boolean(value);
},
async loadDiscoveryConfig() {
try {
const response = await window.api.get(`/api/v1/reticulum/discovery`);
const discovery = response.data?.discovery ?? {};
this.discoveryConfig.discover_interfaces = this.parseBool(discovery.discover_interfaces);
this.discoveryConfig.interface_discovery_sources = discovery.interface_discovery_sources ?? "";
this.discoveryConfig.interface_discovery_whitelist = discovery.interface_discovery_whitelist ?? "";
this.discoveryConfig.interface_discovery_blacklist = discovery.interface_discovery_blacklist ?? "";
this.discoveryConfig.required_discovery_value =
discovery.required_discovery_value !== undefined &&
discovery.required_discovery_value !== null &&
discovery.required_discovery_value !== ""
? Number(discovery.required_discovery_value)
: null;
this.discoveryConfig.autoconnect_discovered_interfaces =
discovery.autoconnect_discovered_interfaces !== undefined &&
discovery.autoconnect_discovered_interfaces !== null &&
discovery.autoconnect_discovered_interfaces !== ""
? Number(discovery.autoconnect_discovered_interfaces)
: 0;
this.discoveryConfig.network_identity = discovery.network_identity ?? "";
} catch (e) {
console.log(e);
}
},
async saveDiscoveryConfig() {
if (this.savingDiscovery) return;
this.savingDiscovery = true;
try {
const payload = {
discover_interfaces: this.discoveryConfig.discover_interfaces,
interface_discovery_sources: this.discoveryConfig.interface_discovery_sources || null,
interface_discovery_whitelist: this.discoveryConfig.interface_discovery_whitelist || null,
interface_discovery_blacklist: this.discoveryConfig.interface_discovery_blacklist || null,
required_discovery_value:
this.discoveryConfig.required_discovery_value === null ||
this.discoveryConfig.required_discovery_value === ""
? null
: Number(this.discoveryConfig.required_discovery_value),
autoconnect_discovered_interfaces:
this.discoveryConfig.autoconnect_discovered_interfaces === null ||
this.discoveryConfig.autoconnect_discovered_interfaces === ""
? 0
: Number(this.discoveryConfig.autoconnect_discovered_interfaces),
network_identity: this.discoveryConfig.network_identity || null,
};

await window.api.patch(`/api/v1/reticulum/discovery`, payload);
ToastUtils.success(this.$t("interfaces.discovery_settings_saved"));
await this.loadDiscoveryConfig();
} catch (e) {
ToastUtils.error(this.$t("interfaces.failed_save_discovery"));
console.log(e);
} finally {
this.savingDiscovery = false;
}
},
toggleDiscoveryActionsMenu(iface) {
const key = this.discoveryKey(iface);
this.openDiscoveryActionKey = this.openDiscoveryActionKey === key ? null : key;
},
normalizeDiscoveryPatternInput(value) {
if (!value) return [];
return String(value)
.replace(/\r?\n/g, ",")
.split(",")
.map((part) => part.trim())
.filter(Boolean);
},
sanitizeDiscoveryPattern(value) {
if (value === null || value === undefined) return "";
return String(value)
.replace(/[\r\n,]/g, "")
.trim()
.slice(0, 128);
},
discoveryFilterCandidates(iface) {
const values = [
iface.name,
iface.type,
iface.reachable_on,
iface.target_host,
iface.remote,
iface.listen_ip,
iface.port,
iface.target_port,
iface.listen_port,
iface.discovery_hash,
iface.transport_id,
iface.network_id,
]
.map((value) => this.sanitizeDiscoveryPattern(value))
.filter(Boolean);
const host = this.sanitizeDiscoveryPattern(
iface.reachable_on || iface.target_host || iface.remote || iface.listen_ip
);
const port = this.sanitizeDiscoveryPattern(iface.port || iface.target_port || iface.listen_port);
if (host && port) {
values.push(`${host}:${port}`);
}
return values.map((value) => value.toLowerCase());
},
matchesDiscoveryGlob(pattern, value) {
const escaped = String(pattern)
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
.replace(/\*/g, ".*");
/* eslint-disable-next-line security/detect-non-literal-regexp -- pattern is escaped above */
const regex = new RegExp(`^${escaped}$`, "i");
return regex.test(value);
},
isDiscoveredBlacklisted(iface) {
const blacklist = this.normalizeDiscoveryPatternInput(
this.discoveryConfig.interface_discovery_blacklist
).map((pattern) => this.sanitizeDiscoveryPattern(pattern).toLowerCase());
if (!blacklist.length) return false;
const candidates = this.discoveryFilterCandidates(iface);
return blacklist.some((pattern) =>
candidates.some((candidate) => this.matchesDiscoveryGlob(pattern, candidate))
);
},
discoveryPatternToken(iface) {
const host = this.sanitizeDiscoveryPattern(
iface.reachable_on || iface.target_host || iface.remote || iface.listen_ip
);
const port = this.sanitizeDiscoveryPattern(iface.port || iface.target_port || iface.listen_port);
if (host && port) return `${host}:${port}`;
return (
host ||
this.sanitizeDiscoveryPattern(iface.transport_id) ||
this.sanitizeDiscoveryPattern(iface.network_id) ||
this.sanitizeDiscoveryPattern(iface.name)
);
},
async addDiscoveredInterfaceToList(iface, action) {
if (this.savingDiscoveryAction) return;
const token = this.discoveryPatternToken(iface);
this.openDiscoveryActionKey = null;
if (!token) {
ToastUtils.error("Unable to identify a filter token from this announce");
return;
}

this.savingDiscoveryAction = true;
try {
const whitelist = this.normalizeDiscoveryPatternInput(
this.discoveryConfig.interface_discovery_whitelist
);
const blacklist = this.normalizeDiscoveryPatternInput(
this.discoveryConfig.interface_discovery_blacklist
);
const tokenLower = token.toLowerCase();
const dedupe = (list) =>
list.filter(
(entry, index, arr) =>
arr.findIndex((candidate) => candidate.toLowerCase() === entry.toLowerCase()) === index
);

let nextWhitelist = [...whitelist];
let nextBlacklist = [...blacklist];
if (action === "allow") {
nextWhitelist.push(token);
nextWhitelist = dedupe(nextWhitelist);
nextBlacklist = nextBlacklist.filter((entry) => entry.toLowerCase() !== tokenLower);
} else {
nextBlacklist.push(token);
nextBlacklist = dedupe(nextBlacklist);
nextWhitelist = nextWhitelist.filter((entry) => entry.toLowerCase() !== tokenLower);
}

const payload = {
interface_discovery_whitelist: nextWhitelist.length ? nextWhitelist.join(",") : null,
interface_discovery_blacklist: nextBlacklist.length ? nextBlacklist.join(",") : null,
};
await window.api.patch(`/api/v1/reticulum/discovery`, payload);
this.discoveryConfig.interface_discovery_whitelist = payload.interface_discovery_whitelist || "";
this.discoveryConfig.interface_discovery_blacklist = payload.interface_discovery_blacklist || "";
ToastUtils.success(
action === "allow"
? `Added ${token} to discovery whitelist`
: `Added ${token} to discovery blacklist`
);
} catch (e) {
ToastUtils.error(this.$t("interfaces.failed_save_discovery"));
console.log(e);
} finally {
this.savingDiscoveryAction = false;
}
},
getDiscoveryIcon(iface) {
switch (iface.type) {
case "AutoInterface":
return "home-automation";
case "RNodeInterface":
return iface.port && iface.port.toString().startsWith("tcp://") ? "lan-connect" : "radio-tower";
case "RNodeMultiInterface":
return "access-point-network";
case "TCPClientInterface":
case "BackboneInterface":
return "lan-connect";
case "TCPServerInterface":
return "lan";
case "UDPInterface":
return "wan";
case "SerialInterface":
return "usb-port";
case "KISSInterface":
case "AX25KISSInterface":
return "antenna";
case "I2PInterface":
return "eye";
case "PipeInterface":
return "pipe";
default:
return "server-network";
}
},
copyToClipboard(text, label) {
if (!text) return;
navigator.clipboard.writeText(text);
ToastUtils.success(`${label} copied to clipboard`);
},
discoveredNetworkName(iface) {
if (!iface) return null;
return iface.network_name || iface.ifac_netname || null;
},
discoveredPassphrase(iface) {
if (!iface) return null;
return iface.passphrase || iface.ifac_netkey || null;
},
maskPassphrase(value) {
if (!value) return "";
const str = String(value);
if (str.length <= 4) return "*".repeat(str.length);
return `${str.slice(0, 2)}${"*".repeat(Math.max(4, str.length - 4))}${str.slice(-2)}`;
},
copyDiscoveredConfigEntry(iface) {
this.openDiscoveryActionKey = null;
const entry = iface?.config_entry;
if (!entry) {
ToastUtils.error(this.$t("interfaces.discovered_no_config"));
return;
}
this.copyToClipboard(entry, this.$t("interfaces.discovered_config_block"));
},
useDiscoveredInterface(iface) {
this.openDiscoveryActionKey = null;
if (!iface) return;
const prefill = {
name: iface.name || "",
type: iface.type || null,
target_host: iface.reachable_on || iface.target_host || iface.remote || null,
target_port: iface.port || iface.target_port || null,
transport_identity: iface.transport_id || iface.transport_identity || null,
network_name: this.discoveredNetworkName(iface),
passphrase: this.discoveredPassphrase(iface),
discoverable: iface.discoverable || null,
config_entry: iface.config_entry || null,
frequency: iface.frequency ?? null,
bandwidth: iface.bandwidth ?? null,
spreadingfactor: iface.sf ?? iface.spreadingfactor ?? null,
codingrate: iface.cr ?? iface.codingrate ?? null,
latitude: iface.latitude ?? null,
longitude: iface.longitude ?? null,
height: iface.height ?? null,
};
try {
if (typeof sessionStorage !== "undefined") {
sessionStorage.setItem("meshchatx.discoveredInterfacePrefill", JSON.stringify(prefill));
}
} catch (e) {
console.log(e);
}
this.$router.push({
name: "interfaces.add",
query: { from_discovered: "1" },
});
},
setStatusFilter(value) {
this.statusFilter = value;
},
filterChipClass(isActive) {
return isActive ? "primary-chip text-xs" : "secondary-chip text-xs";
},
async reloadRns() {
if (this.reloadingRns) return;

try {
this.reloadingRns = true;
ToastUtils.loading(this.$t("app.reloading_rns"), 0, "interfaces-rns-reload");
const response = await window.api.post("/api/v1/reticulum/reload");
ToastUtils.success(response.data.message);
GlobalState.hasPendingInterfaceChanges = false;
GlobalState.modifiedInterfaceNames.clear();
await this.loadInterfaces();
} catch (e) {
ToastUtils.error(e.response?.data?.error || this.$t("interfaces.failed_reload"));
console.error(e);
} finally {
ToastUtils.dismiss("interfaces-rns-reload");
this.reloadingRns = false;
}
},
},
};
Tff7b72</script>

Tff7b72<style scopedTff7b72>
.interfaces-section {
@apply w-full border-b border-gray-200/60 dark:border-zinc-800/60 py-6 sm:py-8;
}
.interfaces-section--hero {
@apply border-b border-gray-200/60 dark:border-zinc-800/60 py-6 sm:py-8;
}
.interfaces-subpanel {
@apply mt-6 pt-6 border-t border-gray-200/50 dark:border-zinc-800/50 first:mt-0 first:pt-0 first:border-0;
}
Tff7b72</style>

Served by rngit 1.5.2 - Generated in 0.08s